/*=============================================================================*
 *  Copyright 2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *=============================================================================*/
package topics.expression.impl;

import topics.Topic;
import topics.TopicSet;
import topics.TopicSpace;
import topics.TopicSpaceSet;
import topics.expression.InvalidTopicExpressionException;
import topics.expression.TopicExpression;
import topics.expression.TopicExpressionEvaluator;
import topics.expression.TopicExpressionException;
import topics.expression.TopicExpressionResolutionException;
import topics.expression.TopicPathDialectUnknownException;
import topics.expression.impl.AbstractTopicExpressionEvaluator;
import topics.v2004_06.TopicsConstants;
import util.xml.NamespaceContext;
import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.StringTokenizer;

/**
 * Topic expression evalutor for the WS-Topics "Full" topic dialect.
 *
 * @see TopicExpressionEvaluator
 *
 * @author Ian Springer (ian DOT springer AT hp DOT com)
 */
public class FullTopicExpressionEvaluator
   extends AbstractTopicExpressionEvaluator
{
   //private static final Log LOG = LogFactory.getLog( FullTopicExpressionEvaluator.class.getName() );
   private static final String[] SUPPORTED_DIALECTS = 
                                                      {
                                                         TopicsConstants.TOPIC_EXPR_DIALECT_FULL
                                                      };

   /**
    * DOCUMENT_ME
    *
    * @return DOCUMENT_ME
    */
   public String[] getDialects(  )
   {
      return SUPPORTED_DIALECTS;
   }

   /**
    * DOCUMENT_ME
    *
    * @param topicSpaceSet DOCUMENT_ME
    * @param topicExpr DOCUMENT_ME
    *
    * @return DOCUMENT_ME
    *
    * @throws TopicPathDialectUnknownException DOCUMENT_ME
    * @throws TopicExpressionResolutionException DOCUMENT_ME
    * @throws InvalidTopicExpressionException DOCUMENT_ME
    * @throws TopicExpressionException DOCUMENT_ME
    */
   public Topic[] evaluate( TopicSpaceSet   topicSpaceSet,
                            TopicExpression topicExpr )
   throws TopicPathDialectUnknownException, 
          TopicExpressionResolutionException, 
          InvalidTopicExpressionException, 
          TopicExpressionException
   {
      String           expr             = getContent( topicExpr );
      NamespaceContext nsContext        = getNamespaceContext( topicExpr );
      StringTokenizer  exprTokenizer    = new StringTokenizer( expr, "|" );
      Set              allMatchedTopics = new HashSet(  );
      while ( exprTokenizer.hasMoreTokens(  ) )
      {
         QName topicPath     = toQName( exprTokenizer.nextToken(  ),
                                        nsContext );
         List  matchedTopics = evaluateTopicPath( topicSpaceSet, topicPath );
         allMatchedTopics.addAll( matchedTopics );
      }

      if ( topicSpaceSet.isFixed(  ) && allMatchedTopics.isEmpty(  ) )
      {
         throw new InvalidTopicExpressionException( "Full topic expression '" + expr
                                                    + "' does not match any topics, and the target topic set is fixed." );
      }

      return (Topic[]) allMatchedTopics.toArray( new Topic[0] );
   }

   private List evaluateTopicPath( TopicSpaceSet topicSpaceSet,
                                   QName         topicPath )
   throws TopicExpressionResolutionException, 
          InvalidTopicExpressionException
   {
      List       matchedTopics = new ArrayList(  );
      TopicSpace topicSpace = getTopicSpace( topicSpaceSet, topicPath );
      if ( topicPath.getLocalPart(  ).indexOf( "///" ) != -1 )
      {
         throw new InvalidTopicExpressionException( "Topic path '" + topicPath
                                                    + "' contains an empty path component." );
      }

      PathTokenizer pathTokenizer     = new PathTokenizer( topicPath.getLocalPart(  ) );
      List          topicSetsToSearch = new ArrayList(  );
      topicSetsToSearch.add( topicSpace );
      boolean atFirstToken = true;
      while ( pathTokenizer.hasMoreTokens(  ) )
      {
         String pathToken = pathTokenizer.nextToken(  );
         matchedTopics.clear(  );
         for ( int i = 0; i < topicSetsToSearch.size(  ); i++ )
         {
            TopicSet topicSetToSearch = (TopicSet) topicSetsToSearch.get( i );
            boolean  recurse = pathToken.startsWith( "/" );
            String   name    = recurse ? pathToken.substring( 1 ) : pathToken;
            matchedTopics.addAll( findTopics( topicSetToSearch, name, recurse ) );
         }

         if ( atFirstToken && matchedTopics.isEmpty(  ) )
         {
            throw new InvalidTopicExpressionException( "Topic path '" + topicPath
                                                       + "' refers to a root topic that is not defined in the referenced topic space." );
         }

         topicSetsToSearch.clear(  );
         topicSetsToSearch.addAll( matchedTopics );
         atFirstToken = false;
      }

      return matchedTopics;
   }

   private List findTopics( TopicSet topicSet,
                            String   name )
   {
      List matchedTopics = new ArrayList(  );
      if ( name.equals( "*" ) )
      {
         Iterator topicIter = topicSet.topicIterator(  );
         while ( topicIter.hasNext(  ) )
         {
            matchedTopics.add( (Topic) topicIter.next(  ) );
         }
      }
      else
      {
         if ( topicSet.containsTopic( name ) )
         {
            matchedTopics.add( topicSet.getTopic( name ) );
         }
      }

      return matchedTopics;
   }

   private List findTopics( TopicSet topicSet,
                            String   name,
                            boolean  recurse )
   throws InvalidTopicExpressionException
   {
      List allMatchedTopics = new ArrayList(  );
      if ( name.equals( "." ) )
      {
         allMatchedTopics.add( topicSet );
         name = "*"; // we only want to evaluate "." the first time through during recursion
      }

      List matchedTopics = findTopics( topicSet, name );
      allMatchedTopics.addAll( matchedTopics );
      if ( recurse )
      {
         Iterator topicIter = topicSet.topicIterator(  );
         while ( topicIter.hasNext(  ) )
         {
            allMatchedTopics.addAll( findTopics( (Topic) topicIter.next(  ), name, recurse ) );
         }
      }

      return allMatchedTopics;
   }

   /**
    * DOCUMENT_ME
    *
    * @version $Revision: 1.8 $
    * @author $author$
    */
   protected class PathTokenizer
   {
      private String m_path;
      private int    m_currentPos;
      private int    m_maxPos;

      /**
       * Creates a new {@link PathTokenizer} object.
       *
       * @param path DOCUMENT_ME
       */
      public PathTokenizer( String path )
      {
         m_path      = path;
         m_maxPos    = m_path.length(  );
      }

      /**
       * DOCUMENT_ME
       *
       * @return DOCUMENT_ME
       */
      public boolean hasMoreTokens(  )
      {
         return m_currentPos < m_maxPos;
      }

      /**
       * DOCUMENT_ME
       *
       * @return DOCUMENT_ME
       *
       * @throws InvalidTopicExpressionException DOCUMENT_ME
       * @throws NoSuchElementException DOCUMENT_ME
       */
      public String nextToken(  )
      throws InvalidTopicExpressionException
      {
         if ( m_currentPos >= m_maxPos )
         {
            throw new NoSuchElementException(  );
         }

         int startPos = m_currentPos;
         m_currentPos = scanToken( startPos );
         String token = m_path.substring( startPos, m_currentPos );
         if ( token.startsWith( "//" ) )
         {
            token = token.substring( 1 );
         }

         ++m_currentPos; // skip the slash
         return token;
      }

      private int scanToken( int startPos )
      throws InvalidTopicExpressionException
      {
         int newPos = startPos;
         if ( newPos == 0 ) // special handling for head of path, as per spec
         {
            if ( m_path.startsWith( "//" ) )
            {
               newPos += 2;
            }

            if ( m_path.charAt( newPos ) == '.' )
            {
               throw new InvalidTopicExpressionException( "'.' may not be used as the first component of a topic path." );
            }
         }
         else
         {
            if ( m_path.charAt( newPos ) == '/' )
            {
               newPos += 1; // do not count leading slash as a delim
            }
         }
         while ( newPos < m_maxPos )
         {
            if ( m_path.charAt( newPos ) == '/' )
            {
               break;
            }

            newPos++;
         }

         return newPos;
      }
   }
}